import { Component, cls, type StyledProps, unsafe } from '@knno/jsx';
import type { EventBus } from '../../tools/eventBus';
import { isDescendant } from '../../tools/utils';
import { contentEditable } from '../contentEditable';
import { defineElement } from '../defineElement';
import {
	askInsertImage,
	askInsertTable,
	convertToDataURL,
	findCursorElement,
	findParent,
	getRtfImages,
	isBRElement,
	isImageElement,
	isParagraphElement,
	isTableElement,
	isTextNode,
	loadImageToDataURL,
	type TrimNodeContext,
} from '../editor';
import { DragMask } from '../mask/mask';
import { createImage, imageOnKeyDown } from './image';
import { editorStyle } from './inlineEditor.css';
import { paragraphOnContextMenu, paragraphOnKeyDown } from './paragraph';
import {
	type CellInfo,
	calculateCells,
	createTable,
	getAllColumnWidth,
	getCellInfo,
	parseTable,
	setAllColumnWidth,
	tableOnContextMenu,
	tableOnContextMenuMultipleCells,
	tableOnKeyDown,
} from './table';

export type ToolbarData = {
	editor?: InlineEditor;
	activeElements?: Element[];
};

export type EditCommand = {
	toolbarChange: ToolbarData | undefined;
};

export const TOOLBAR_CHANGE = 'toolbarChange';

export type EditorEventBus = EventBus<EditCommand>;

export type InlineEditorProps = StyledProps<InlineEditor> & {
	source?: string;
	eventBus?: EditorEventBus;
	editorCollector?: Set<InlineEditor>;
	readOnly?: boolean;
	disabled?: boolean;
	onChange?: (this: InlineEditor) => void;
	onConnected?: (this: InlineEditor) => void;
	onDisconnected?: (this: InlineEditor) => void;
	onFocus?: (this: InlineEditor) => void;
	onBlur?: (this: InlineEditor) => void;
};

const RichElement = defineElement('knno-rich-editor', HTMLElement);

export class InlineEditor extends Component<InlineEditorProps, InlineEditor, HTMLElement> {
	private selectedTable: HTMLTableElement | undefined;
	private selectedCells: CellInfo[] = [];

	onSelectionChange(): boolean {
		const elem = this.getActived();
		if (elem) {
			this.refreshToolbar();
			return true;
		} else {
			return false;
		}
	}

	override render(): Node {
		const src = this.props?.source;
		let beginCell: HTMLTableCellElement | undefined;
		let endCell: HTMLTableCellElement | undefined;
		let selectMode = false;
		const root = (
			<RichElement
				contenteditable={contentEditable}
				class={cls(
					this.props.class,
					editorStyle,
					this.props.readOnly ? 'readOnly' : '',
					this.props.disabled ? 'disabled' : '',
				)}
				style={this.props.style}
				onInput={() => {
					this.emitChange();
				}}
				onConnected={() => {
					this.props.editorCollector?.add(this);
					this.props.onConnected?.call(this);
				}}
				onDisconnected={() => {
					this.props.editorCollector?.delete(this);
					this.props.onDisconnected?.call(this);
				}}
				onKeyUp={(evt) => {
					if ((evt.key === 'Backspace' || evt.key === 'Delete') && !evt.ctrlKey && !evt.altKey) {
						if (
							Array.from(root.children).filter((c) => c.nodeName.toLowerCase() !== 'br').length ===
							0
						) {
							root.querySelectorAll(':scope>br').forEach((br) => root.removeChild(br));
							const p = (
								<p>
									<br />
								</p>
							);
							root.appendChild(p);
							const sel = window.getSelection();
							if (!sel) return;
							const range = document.createRange();
							range.selectNodeContents(p);
							range.collapse();
							sel.removeAllRanges();
							sel.addRange(range);
						}
					}
				}}
				onKeyDown={(evt) => {
					const target = evt?.target as HTMLElement | null;
					if (!target) return;
					const elem = findCursorElement();
					if (!elem) {
						if (evt.key === 'Backspace' || evt.key === 'Delete') {
							this.selectedCells.forEach((cell) => {
								if (cell.td) {
									cell.td.innerHTML = '<br>';
								}
							});
							this.emitChange();
						}
						return;
					}
					if (evt.key === 'a' && evt.ctrlKey) {
						if (!this.isSelectAll()) {
							this.selectAll();
							evt.preventDefault();
							evt.stopPropagation();
						}
						return;
					}
					if (elem.nodeName === 'P' && elem.parentElement === root) {
						if (evt.key === 'v' && evt.ctrlKey) {
							// evt.preventDefault();
							// evt.stopPropagation();
							// this.paste().then(() => this.emitChange());
						} else {
							paragraphOnKeyDown(this, elem as HTMLParagraphElement, evt);
						}
					} else if (elem.nodeName === 'DIV' && elem.parentElement === root) {
						imageOnKeyDown(this, elem as HTMLParagraphElement, evt);
					} else if (elem.nodeName === 'TD') {
						if (evt.key === 'Enter' && evt.ctrlKey) {
							evt.preventDefault();
							const tb = findParent<HTMLTableElement>(elem, 'table');
							if (!tb) return null;
							const sel = window.getSelection();
							if (!sel || sel.rangeCount === 0) return null;
							sel.empty();
							const range = document.createRange();
							range.selectNode(tb);
							range.collapse(evt.shiftKey);
							const p = (
								<p>
									<br />
								</p>
							);
							range.insertNode(p);
							sel.addRange(range);
							range.selectNodeContents(p);
							range.collapse();
						} else {
							tableOnKeyDown(this, elem as HTMLTableCellElement, evt);
						}
					} else if (elem === root) {
						if (evt.key === 'Enter') {
							evt.preventDefault();
							const sel = window.getSelection();
							if (!sel || sel.rangeCount === 0) return null;
							const range = sel.getRangeAt(0);
							range.insertNode(
								<p>
									<br />
								</p>,
							);
							range.collapse();
						} else if (evt.key === 'Backspace') {
							const sel = window.getSelection();
							if (!sel || sel.rangeCount === 0) return null;
							const range = sel.getRangeAt(0);
							if (
								range.startContainer === range.endContainer &&
								range.startOffset === range.endOffset &&
								root.childNodes[range.startOffset]?.nodeName === 'DIV'
							) {
								evt.preventDefault();
								evt.preventDefault();
								const div = root.childNodes[range.startOffset] as HTMLElement;
								if (div.previousElementSibling) {
									if (div.previousElementSibling.nodeName !== 'P') {
										range.selectNode(div.previousElementSibling);
										range.deleteContents();
									} else {
										if (div.previousElementSibling.textContent === '') {
											range.selectNode(div.previousElementSibling);
											range.deleteContents();
										} else {
											range.selectNodeContents(div.previousElementSibling);
											range.collapse(false);
										}
									}
								}
							}
						}
						this.emitChange();
					}
				}}
				onContextMenu={(evt) => {
					evt.preventDefault();
					const target = evt?.target as HTMLElement | null;
					if (!target) return;
					const elem = findCursorElement();
					if (!elem) return;
					if (elem.nodeName === 'P') {
						paragraphOnContextMenu(this, elem as HTMLParagraphElement, evt);
					} else if (elem.nodeName === 'TD') {
						tableOnContextMenu(this, target as HTMLTableCellElement, evt);
					}
				}}
				onPointerDown={(evt) => {
					beginCell = undefined;
					endCell = undefined;
					selectMode = false;
					if (evt.button === 2 && this.selectedCells.length > 0) {
						this.props.eventBus?.send(TOOLBAR_CHANGE, {
							editor: this,
							activeElements: this.selectedCells
								.filter((cell) => cell.td)
								.map((cell) => cell.td as HTMLElement),
						});
						tableOnContextMenuMultipleCells(this, this.selectedCells, evt, () => {
							this.refreshToolbar();
						});
						evt.preventDefault();
						return;
					}
					this.clearTableSelection();
					const target = evt?.target as HTMLElement | null;
					if (!target || evt.button !== 0) return;
					if (target.nodeName === 'TD') {
						const table = findParent<HTMLTableElement>(target, 'table') ?? undefined;
						if (table) {
							const tableInfo = parseTable(table);
							// const table = findParent<HTMLTableElement>(target, 'table');
							const cellInfo = getCellInfo(tableInfo, target as HTMLTableCellElement);
							if (cellInfo) {
								if (evt.offsetX < 5 || evt.offsetX > target.offsetWidth - 5) {
									const tableColumnWidth = getAllColumnWidth(tableInfo);
									let left: number;
									let right: number;
									let leftWidth: number;
									let rightWidth: number;
									if (evt.offsetX < 5) {
										left = cellInfo.column - 1;
										right = cellInfo.column;
									} else {
										left = cellInfo.column;
										right = cellInfo.column + 1;
										if (right >= tableColumnWidth.length) {
											right = -1;
										}
									}
									if (left >= 0) {
										leftWidth = tableColumnWidth[left];
									}
									if (right >= 0) {
										rightWidth = tableColumnWidth[right];
									}
									// console.log('columns:', left, right);
									// console.log('src width:', tableColumnWidth);
									DragMask.show(evt, 'col-resize', (ev, distance) => {
										if (left >= 0) {
											tableColumnWidth[left] = leftWidth + distance.x;
										}
										if (right >= 0) {
											tableColumnWidth[right] = rightWidth - distance.x;
										}
										// console.log(tableColumnWidth);
										setAllColumnWidth(tableInfo, tableColumnWidth);
										this.emitChange();
									});
									evt.preventDefault();
									return;
								}
							}
						}
						this.selectedTable = findParent<HTMLTableElement>(target, 'table') ?? undefined;
						if (this.selectedTable) {
							beginCell = target as HTMLTableCellElement;
						}
					}
				}}
				onPointerMove={(evt) => {
					const target = evt?.target as HTMLElement | null;
					if (!target) return;
					if (target.nodeName === 'TD' && beginCell) {
						endCell = target as HTMLTableCellElement;
						const table = findParent<HTMLTableElement>(target, 'table') ?? undefined;
						if (table && table === this.selectedTable && (endCell !== beginCell || selectMode)) {
							selectMode = true;
							const selection = window.getSelection();
							if (selection && selection.rangeCount > 0) {
								selection.removeAllRanges();
							}
							table.querySelectorAll('td').forEach((td) => (td.className = ''));
							this.selectedCells = calculateCells(parseTable(table), [beginCell, endCell]);
							this.selectedCells.forEach((cell) => {
								if (cell.td) {
									cell.td.className = 'selected';
								}
							});
							this.props.eventBus?.send(TOOLBAR_CHANGE, {
								editor: this,
								activeElements: this.selectedCells
									.filter((cell) => cell.td)
									.map((cell) => cell.td as HTMLElement),
							});
						}
					} else if (target.nodeName === 'TD') {
						const table = findParent<HTMLTableElement>(target, 'table') ?? undefined;
						if (table) {
							// const table = findParent<HTMLTableElement>(target, 'table');
							if (evt.offsetX < 5 || evt.offsetX > target.offsetWidth - 5) {
								table.className = 'change-column';
							} else {
								table.className = '';
							}
						}
					}
				}}
				onPointerUp={(evt) => {
					const target = evt?.target as HTMLElement | null;
					if (!target) return;
					beginCell = undefined;
					endCell = undefined;
					selectMode = false;
					if (target.nodeName === 'TD') {
						endCell = target as HTMLTableCellElement;
					}
				}}
				onDragOver={(evt) => {
					evt.preventDefault();
					const elem = findCursorElement();
					if (!elem) return;
					if (elem.nodeName === 'P' && elem.parentElement === root) {
						if (evt.dataTransfer) {
							evt.dataTransfer.dropEffect = 'copy';
						}
					} else {
						if (evt.dataTransfer) {
							evt.dataTransfer.dropEffect = 'none';
						}
					}
				}}
				onDrop={(evt) => {
					evt.preventDefault();
					const elem = findCursorElement();
					if (!elem) return;
					if (elem.nodeName === 'P' && elem.parentElement === root) {
						const clipboardData = evt.dataTransfer;
						if (!clipboardData) return;
						this.paste(clipboardData).then(() => this.emitChange());
					}
				}}
				onPaste={(evt) => {
					const elem = findCursorElement();
					if (!elem) return;
					if (elem.nodeName === 'P' && elem.parentElement === root) {
						evt.preventDefault();
						evt.stopPropagation();
						const clipboardData = evt.clipboardData;
						if (!clipboardData) return;
						this.paste(clipboardData).then(() => this.emitChange());
					}
				}}
				onFocus={() => {
					this.props.onFocus?.call(this);
				}}
				onBlur={() => {
					this.props.onBlur?.call(this);
				}}
			>
				{src ? (
					unsafe(src)
				) : (
					<p>
						<br />
					</p>
				)}
			</RichElement>
		) as HTMLElement;
		return root;
	}

	private insertNode(node: Node, range: Range, p: Element) {
		range.deleteContents();
		range.setEndAfter(p);
		const text = range.extractContents()?.textContent ?? '';
		this.el.insertBefore(node, p.nextSibling);
		if (p.textContent === '') {
			p.remove();
		}
		if (text) {
			this.el.insertBefore(<p>{text}</p>, node.nextSibling);
		}
	}

	insertTable() {
		const elem = findCursorElement(false);
		if (!elem) return;
		if (elem.nodeName !== 'P') return;
		const sel = window.getSelection();
		if (!sel || sel.rangeCount === 0) return;
		const range = sel.getRangeAt(0);
		if (!range) return;
		askInsertTable((rows, cols) => {
			this.insertNode(createTable(rows, cols), range, elem);
			this.emitChange();
		});
	}

	insertImage() {
		const elem = findCursorElement(false);
		if (!elem) return;
		if (elem.nodeName !== 'P') return;
		const sel = window.getSelection();
		if (!sel || sel.rangeCount === 0) return;
		const range = sel.getRangeAt(0);
		if (!range) return;
		askInsertImage((src) => {
			const imgNode = createImage(src);
			this.insertNode(imgNode, range, elem);
			this.emitChange();
		});
	}

	deleteTable(table: HTMLTableElement) {
		table.remove();
		if (this.el.children.length === 0) {
			const p = (
				<p>
					<br />
				</p>
			);
			this.el.append(p);
			const selection = window.getSelection();
			const range = document.createRange();
			range.selectNodeContents(p);
			range.collapse(false);
			selection?.removeAllRanges();
			selection?.addRange(range);
			this.emitChange();
		}
	}

	// paste html content should be trimed to allowed style.
	async paste(clipboardData: DataTransfer) {
		//const win32 = YDY.app.getSysInfo().deviceInfo.osType === 'win32';
		const elem = findCursorElement(false);
		if (!elem) return;
		if (elem.nodeName !== 'P') return;
		const sel = window.getSelection();
		if (!sel || sel.rangeCount === 0) return;
		const range = sel.getRangeAt(0);
		range.deleteContents();
		range.collapse();
		function trimNode(node: Node, context: TrimNodeContext) {
			if (/^(style|script|meta|title|head|#comment)$/i.test(node.nodeName)) return;
			if (isTextNode(node) || isBRElement(node)) {
				if (context.pureText) {
					context.pureTextFragment.appendChild(node);
				} else {
					if (context.paragraph) {
						context.paragraph.appendChild(node);
					} else {
						const p = document.createElement('p');
						p.appendChild(node);
						context.destFragment.appendChild(p);
						context.paragraph = p;
					}
				}
			} else if (isTableElement(node)) {
				context.pureText = false;
				context.paragraph = undefined;
				Array.from(node.tBodies).forEach((section) => {
					Array.from(section.rows).forEach((row) => {
						Array.from(row.attributes).forEach((attr) => row.removeAttribute(attr.name));
						Array.from(row.cells).forEach((cell) => {
							if (cell.nodeName.toLowerCase() === 'th') {
								const td = document.createElement('td');
								td.textContent = cell.textContent;
								if (td.childNodes.length === 0) {
									td.appendChild(document.createElement('br'));
								}
								Array.from(cell.attributes)
									.filter((attr) => /^(rowspan|colspan)$/i.test(attr.name))
									.forEach((attr) => td.setAttribute(attr.name, attr.value));
								row.insertBefore(td, cell);
								row.removeChild(cell);
							} else {
								cell.textContent = cell.textContent ?? '';
								if (cell.childNodes.length === 0) {
									cell.appendChild(document.createElement('br'));
								}
								Array.from(cell.attributes)
									.filter((attr) => !/^(rowspan|colspan)$/i.test(attr.name))
									.forEach((attr) => cell.removeAttribute(attr.name));
							}
						});
						node.appendChild(row);
					});
					node.removeChild(section);
				});
				Array.from(node.attributes).forEach((attr) => node.removeAttribute(attr.name));
				context.destFragment.appendChild(node);
			} else if (isImageElement(node)) {
				context.pureText = false;
				context.paragraph = undefined;
				Array.from(node.attributes)
					.filter((attr) => attr.name !== 'src')
					.forEach((attr) => node.removeAttribute(attr.name));
				// convert image src
				if (!/^data:/.test(node.src)) {
					const url = node.src;
					node.src = '';
					if (url.startsWith('file:///')) {
						const blob = context.images.shift();
						if (blob) {
							// node.src = blob;
							convertToDataURL(blob)
								.then((dataUrl) => (node.src = dataUrl))
								.catch((reason) => console.error(reason));
						}
					} else {
						loadImageToDataURL(url)
							.then((dataUrl) => (node.src = dataUrl))
							.catch((reason) => console.error(reason));
					}
				}
				node.style.maxWidth = '100%';
				const div = document.createElement('div');
				div.appendChild(node);
				context.destFragment.appendChild(div);
			} else if (isParagraphElement(node)) {
				context.pureText = false;
				const p = document.createElement('p');
				context.destFragment.appendChild(p);
				context.paragraph = p;
				node.childNodes.forEach((child) => trimNode(child, context));
			} else {
				node.childNodes.forEach((child) => trimNode(child, context));
			}
		}
		try {
			//const items = await navigator.clipboard.read();
			const context = {
				pureTextFragment: document.createDocumentFragment(),
				destFragment: document.createDocumentFragment(),
				pureText: true,
				images: [],
			};
			if (clipboardData.types.includes('text/html')) {
				if (clipboardData.types.includes('text/rtf')) {
					getRtfImages(clipboardData.getData('text/rtf'), context);
				}
				const html = clipboardData.getData('text/html');
				const fragment = range.createContextualFragment(html);
				Array.from(fragment.childNodes).forEach((node) => trimNode(node, context));
			} else if (clipboardData.types.includes('text/plain')) {
				const text = clipboardData.getData('text/plain');
				trimNode(document.createTextNode(text), context);
			}
			for (let i = 0; i < clipboardData.files.length; i++) {
				const file = clipboardData.files[i];
				if (/^image\/(png|jpeg|svg\+xml)$/.test(file.type)) {
					const url = await convertToDataURL(file);
					trimNode(
						<div>
							<img src={url} alt="" />
						</div>,
						context,
					);
				}
			}
			if (context.pureTextFragment.hasChildNodes()) {
				range.insertNode(context.pureTextFragment);
				range.collapse();
			}
			if (context.destFragment.hasChildNodes()) {
				range.setEnd(range.startContainer, range.startContainer.childNodes.length);
				const otherPart = range.extractContents();
				const firstElement = context.destFragment.firstChild;
				if (firstElement?.nodeName === 'P') {
					firstElement.appendChild(otherPart);
				} else {
					const newParagraph = document.createElement('P');
					newParagraph.appendChild(otherPart);
					context.destFragment.insertBefore(newParagraph, context.destFragment.firstChild);
				}
				range.setEndAfter(elem);
				range.collapse();
				range.insertNode(context.destFragment);
				range.collapse();
			}
		} catch (err) {
			console.error('Paste failed:', err);
		}
	}

	clearTableSelection() {
		this.selectedCells = [];
		this.selectedTable?.querySelectorAll('td')?.forEach((td) => (td.className = ''));
	}

	getActived(): Element | null {
		const elem = findCursorElement();
		this.clearTableSelection();
		if (!elem || !isDescendant([this.el], elem)) {
			return null;
		} else {
			return elem;
		}
	}

	isSelectAll(): boolean {
		const sel = window.getSelection();
		if (!sel || sel.rangeCount === 0) return false;
		const range = sel.getRangeAt(0);
		return (
			range.startContainer === this.el &&
			range.endContainer === this.el &&
			range.startOffset === 0 &&
			range.endOffset === this.el.childNodes?.length
		);
	}

	selectAll() {
		const rootElement = this.el;
		if (!rootElement) return;
		const sel = window.getSelection();
		if (!sel) return;
		sel.removeAllRanges();
		const range = document.createRange();
		range.selectNodeContents(rootElement);
		sel.addRange(range);
	}

	refreshToolbar() {
		const elem = this.getActived();
		if (elem) {
			this.props.eventBus?.send(TOOLBAR_CHANGE, { editor: this, activeElements: [elem] });
		}
	}

	private updateTimer: number | undefined;
	emitChange() {
		if (this.updateTimer) {
			clearTimeout(this.updateTimer);
		}
		this.updateTimer = setTimeout(() => {
			this.updateTimer = undefined;
			// this.attrs.data[this.attrs.field] = this.html();
			this.el.dispatchEvent(new Event('change', { bubbles: true }));
		}, 50);
	}

	onChange(handler: ((this: InlineEditor) => void) | undefined): this {
		this.props.onChange = handler;
		return this;
	}

	onConnected(handler: ((this: InlineEditor) => void) | undefined): this {
		this.props.onConnected = handler;
		return this;
	}

	onDisconnected(handler: ((this: InlineEditor) => void) | undefined): this {
		this.props.onDisconnected = handler;
		return this;
	}

	onFocus(handler: ((this: InlineEditor) => void) | undefined): this {
		this.props.onFocus = handler;
		return this;
	}

	onBlur(handler: ((this: InlineEditor) => void) | undefined): this {
		this.props.onBlur = handler;
		return this;
	}

	get readOnly(): boolean {
		return this.props.readOnly ?? false;
	}

	setReadOnly(value: boolean): this {
		this.props.readOnly = value;
		this.el.classList.toggle('readOnly', value);
		if (value) {
			this.el.setAttribute('contenteditable', contentEditable);
		} else {
			this.el.removeAttribute('contenteditable');
		}
		return this;
	}

	get disabled(): boolean {
		return this.props.disabled ?? false;
	}

	setDisabled(value: boolean): this {
		this.props.disabled = value;
		this.el.classList.toggle('disabled', value);
		return this;
	}
}
